由於篇幅的關係,這裡只介紹常見的陣列操作。
map
filter
reduce
every
some
為什麼陣列的操作那麼重要?雖然沒有這些方法你也可以做開發,以及做到完全一樣的事情,但是這些高階操作能夠幫助你、以及你的夥伴更容易清楚你的程式碼在做什麼。
最主要的差別在於聲明式與命令式。舉例來說,如果我們想要反轉一個字串:
// 命令式
var str = "abcd";
var result = "";
for (let i = str.length -1; i >= 0; i--) {
result += str[i];
}
// 聲明式
"abcd".reverse();
可以發現幾件事情:
for...loop
迴圈要知道在寫程式的時候最討厭 for...loop 的變數條件用錯,或是一個不小心讓迴圈的變數變成全域變數
這看起來有點像...把實作包裝成函數?
可以這麼說,但怎麼設計這個函數,讓他提供足夠的抽象,同時又能具備一定程度的彈性,是在寫聲明式時要注意的事情。
這樣寫可以更有效幫助我們除錯,也更容易表達出這段程式碼想要做什麼事情。或許有人會懷疑會不會讓效能變差,不過除非你的程式已經到需要這樣斤斤計較的地步,不然犧牲一點點的效能,可以換來易讀性更高的程式碼。
以下介紹一些陣列當中常見的操作:
能夠將陣列轉換成新的陣列。
// 將數字 * 2
const arr = [1,2,3,4];
arr.map(val => val * 2);
function multiply(n) {
return (val) => {
return val * n;
}
}
arr.map(multiply(2));
// 轉換 data field
const data = [
{ Name: 'kalan', Age: 22, Introduction: { Brief: 'xxxx', Details: 'longlongtext'}},
{ Name: 'kalan2', Age: 22, Introduction: { Brief: 'xxxx2', Details: 'longlongtext2'}}
];
data.map(user => {
return {
name: user.Name,
age: user.Age,
introduction: user.Introduction.Brief,
};
});
我們看到第一個案例,簡單地將所有數字 * 2。另外一個比較實用的案例是,我們將回傳的資料先轉換成小寫,並取出我們想要的欄位,轉換後的資料比較容易拿來使用。比起用 for(let i = 0; i < arr.length; i++)
程式碼更簡潔,程式碼要表達的意圖也相當明顯。
除非你在做演算法相關的試題或應用,用迴圈比較好處理,或是你正在拼命榨出效能,不然處理資料的部分通常可以用 map 等高階操作搞定。
警告!!!
特別要注意的是,因為這個函數大家期待的是一個新的 array,所以強烈建議不要在裡頭做任何 side effect 的操作,像是 console.log
,呼叫 API,更改 DOM 等等,因為大家並不預期裡頭會有這樣的操作,就算你相當清楚你在做什麼,過了一個禮拜後你還是有可能忘記,並且埋下了一個或許不好發現的 bug。
說起來 filter 這個 API 命名其實有點不明確,因為有時常常會想,到底是把符合條件的值 filter 掉,還是把符合條件的值留下來?
在做資料查詢,或是你想要根據某個條件過濾值的時候相當好用:
const arr = [1,2,3,4,5];
arr.filter(val => val % 2 === 0); // [2,4];
// 挑出已成年的人
const data = [
{ id: 1, age: 18 },
{ id: 2, age: 20 },
{ id: 3, age: 14 },
];
data.filter(val => val.age >= 18);
reduce 的妙用相當多,最常見的就是將陣列組合成一個值,例如計算總和:
const arr = [1,2,3,4,5];
arr.reduce((acc, curr) => {
acc += curr;
return acc;
}, 0)
或是把陣列全部塞到物件裡頭:
const arr = [
{id: 'a', name: 'wu' },
{id: 'b', name: 'ka'},
{id: 'c', name: 'jack'},
];
const result = arr.reduce((acc, curr) => {
return Object.assign(acc, {
[curr.id]: curr
});
}, {});
console.log(result)
// {a: {id: "a", name: "wu"}
// b: {id: "b", name: "ka"}
// c: {id: "c", name: "jack"}}
如同它的名字 every
一樣,會根據回傳值,來判定只有當陣列全部的值都為 true 才回傳 true。例如我想要這個陣列裡頭全部都是 18 歲以上的人,就可以這樣寫:
const people = [
{ age: 18 },
{ age: 20 },
{ age: 22 },
];
const isAllAdult = people.every(p => p.age >= 18); // true
和 every 類似的函數,不過只要其中一個元素符合傳入的函式就會回傳 true。
const messages = [
'hello',
'hello world',
'apple'
];
const containHello = messages.some(msg => msg.includes('hello')); // true
為什麼會特別介紹這些 API 是因為剛開始為了學習方便會習慣用 for loop 的方式寫,如果沒有常常去看其他人的程式碼或是和別人合作過,難免就這樣繼續寫下去了,但是在處理資料或是寫應用的時候,寫這些程式碼不只會多出好幾個變數,閱讀起來也不是那麼方便,往往要看到迴圈內的操作才能明白想要幹嘛,因此或許已經有很多人都知道,但還是花了一個章節來談論它。
除此之外,在 JavaScript 以及 ES6 語法當中,也有許多可以讓 code 看起來更簡潔的小技巧,這裡就不多贅述。
雖然 JavaScript 中有很多奇技淫巧,但正常的情況下你應該避免使用它。我們不應該為了少寫一點點的程式碼而選用一個晦澀難懂的方式。任何語言都是這樣。
了解陣列的操作後,我們試著從設計 API 角度出發,實作看看 map。
根據 MDN 上的描述,一個 map 的函式簽名是這樣的:
var new_array = arr.map(function callback(currentValue[, index[, array]]) {
// Return element for new_array
}[, thisArg])
第一個參數是一個函數,回傳的值將會成為陣列的新元素,第二個可選參數是 this
,你可以綁定自己的 this。
首先要思考的是,如果用 prototype 來實作 map 這個 function,那麼 this 會是什麼?在 Array.prototype 中的話,this
就會是整個陣列:
const arr = [];
arr.map()
Array.prototype.map = function(fn, thisArg) {
const result = [];
for (let i = 0; i < this.length; i++) {
result.push(fn.call(thisArg || null), this[i], i, this));
}
return result;
};
一個簡單的函式實作可以看到 API 設計時可以考量的地方:
thisArg
的綁定傳入特定的 context。在建立函數的時候也有煩惱不知道要參數化哪些地方嗎?試著從原生的 API 中思考一下吧!
原來以原生的 JS 所寫出來的方法稱「命令式」,一直以為這種寫法可以稱為 polyfile ....
感謝出好文,受益良多~
之前看過如何 flat 的方法,用 reduce 也相當好用啊
let array = [1, 2, [5, 6]]
array.reduce((cur, acc) => {
console.log(acc)
return cur.concat(acc)
}, [])